package com.lang.downloader.http.LifeCycle;

import com.alibaba.fastjson.JSON;
import com.lang.DownloadLifeCycle;
import com.lang.downloader.http.config.CacheFile;
import com.lang.downloader.http.config.HttpDownloadConfig;
import com.lang.downloader.http.exception.HttpDownloadException;
import com.lang.util.DownloadConfigManager;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * Http下载 生命周期
 */
@Slf4j
public class HttpDownloadLifeCycle implements DownloadLifeCycle<HttpDownloadConfig> {

    /**
     * 是否为断点下载
     *
     * @param config 配置信息
     * @return true 是断点下载， false 是初次开始下载
     */
    @Override
    public boolean isRestart(HttpDownloadConfig config) {
        String url = config.getUrl();
        String configFile = config.getConfigFilePath();
        Path savePath = Paths.get(config.getSavePath());
        Path configFilePath = Paths.get(configFile);
        Path tempConfigFilePath = Paths.get(DownloadConfigManager.tempConfigFile(config.getConfigFilePath()));
        boolean isConfigFileExists = (Files.notExists(configFilePath) && Files.notExists(tempConfigFilePath));
        if (isConfigFileExists) {
            log.info("配置文件={}不存在, 文件={} 定义为 初次下载 下载链接={}", configFile, savePath.getFileName(), url);
            return false;
        } else { //配置文件和临时配置文件都不存在， 就视为初次下载
            log.info("根据配置文件={}信息，重启下载;文件={} 下载链接={}", configFile, savePath.getFileName(), url);
            return true;
        }
    }

    /**
     * 从配置文件中检查下载是否已经完成
     *
     * @param config 配置信息
     * @return true已完成 false未完成
     */
    @Override
    public boolean checkTaskDone(HttpDownloadConfig config) {
        String url = config.getUrl();
        Path theDownloadFilePath = Paths.get(config.getSavePath());
        if (config.getContentLength() != config.getHasDone().get()) {
            log.info("文件下载流长度和已下载流长度不相同 文件={} url={} 未下载完成", theDownloadFilePath.getFileName(), url);
            return false;
        }
        if (!config.isDone()) {
            log.info("配置文件记录下载未完成 文件={} url={}", theDownloadFilePath.getFileName(), url);
            return false;
        }
        if (Files.notExists(theDownloadFilePath)) {
            List<CacheFile> cacheFiles = config.getTempFiles();
            if (cacheFiles == null || cacheFiles.isEmpty()) {
                throw new HttpDownloadException(String.format("配置信息异常, 没有要下载的文件, url=%s 文件=%s", url, theDownloadFilePath.getFileName()));
            }
            Set<CacheFile> doneFiles = cacheFiles.stream().filter(CacheFile::isDone).filter(cacheFile -> Files.exists(Paths.get(cacheFile.getFilePath())))
                    .collect(Collectors.toSet());
            if (cacheFiles.size() != doneFiles.size()) {
                log.info("下载的文件不存在 文件={} url={} 未下载完成", theDownloadFilePath.getFileName(), url);
                return false;
            } else {
                log.info("文件未写入完成 但文件流已下载完成 准备合并文件 文件={}", theDownloadFilePath.getFileName());
                writeFile(config);
                return true;
            }
        }
        if (theDownloadFilePath.toFile().length() != config.getContentLength()) {
            log.info("已下载的文件 文件大小和文件流大小不同 文件={} url={} 未下载完成", theDownloadFilePath.getFileName(), url);
            return false;
        }
        return true;
    }

    /**
     * 检查配置文件中的保存路径是否可用 并创创建输出流
     * 1.是否配置文件保存路径
     * 2.如果文件存在 检查覆盖参数是否允许覆盖
     * 3.尝试创建文件输出流
     *
     * @param config 配置信息
     */
    @Override
    public void saveFileOutputStream(HttpDownloadConfig config) {
        if (config.getOutputStream() != null) {
            log.info("文件{} 输出文件流已初始化", Paths.get(config.getSavePath()).getFileName());
            return;
        }
        if (StringUtils.isBlank(config.getSavePath()))
            throw new HttpDownloadException("未配置文件保存路径 url=" + config.getUrl());
        Path savePath = Paths.get(config.getSavePath());
        if (Files.exists(savePath)) { //文件已存在
            if (savePath.toFile().length() == 0) { //文件为空文件
                try {
                    if (!Files.deleteIfExists(savePath)) { //把空文件删除
                        throw new HttpDownloadException(String.format("空文件=%s 删除失败", savePath.getFileName()));
                    }
                } catch (IOException e) {
                    throw new HttpDownloadException(String.format("空文件=%s 删除失败", savePath.getFileName()), e);
                }
            } else { //文件不是空文件
                if (config.isOverWrite()) { //如果允许覆盖下载
                    try {
                        if (Files.deleteIfExists(savePath)) {
                            throw new HttpDownloadException(String.format("覆盖下载时 删除原文件=%s失败", savePath.getFileName()));
                        }
                    } catch (IOException e) {
                        throw new HttpDownloadException(String.format("覆盖下载时 删除原文件=%s失败", savePath.getFileName()), e);
                    }
                } else {
                    throw new HttpDownloadException(String.format("需下载的文件=%s已存在, 且下载配置不可覆盖下载", savePath.getFileName()));
                }
            }
        }
        try {
            config.setOutputStream(Files.newOutputStream(savePath));
        } catch (IOException e) {
            throw new HttpDownloadException(String.format("创建文件=%s输出流失败", savePath.getFileName()), e);
        }
    }

    /**
     * 当下载开始时的操作
     *
     * @param config 配置信息
     */
    @Override
    public void whenStart(HttpDownloadConfig config) {
        Path savePath = Paths.get(config.getSavePath());
        String url = config.getUrl();
        log.info("开始下载 文件={} url={}", savePath.getFileName(), url);
    }

    /**
     * 当正常下载完成时的操作
     *
     * @param config 配置信息
     */
    @Override
    public void whenDone(HttpDownloadConfig config) {
        Path savePath = Paths.get(config.getSavePath());
        String url = config.getUrl();
        List<CacheFile> cacheFiles = config.getTempFiles();
        OutputStream outputStream = config.getOutputStream();
        if (outputStream == null)
            throw new HttpDownloadException(String.format("未初始化文件输出流 %s", savePath.getFileName()));
        if (cacheFiles != null && !cacheFiles.isEmpty()) {
            writeFile(config);
        } else {
            throw new HttpDownloadException(String.format("文件%s下载失败, 没有下载成功的信息流, url=%s", savePath.getFileName(), url));
        }
        config.setDone(true);
        DownloadConfigManager.writeDownloadConfig(config.getConfigFilePath(), config);
        log.info("文件{}下载已完成", savePath.getFileName());
    }

    /**
     * 合并临时文件
     *
     * @param config 配置信息
     */
    private void writeFile(HttpDownloadConfig config) {
        Path savePath = Paths.get(config.getSavePath());
        List<CacheFile> cacheFiles = config.getTempFiles();
        OutputStream outputStream = config.getOutputStream();
        byte[] bufferByte = new byte[config.getBufferSize()];
        Set<CacheFile> doneFiles = cacheFiles
                .stream()
                .filter(CacheFile::isDone) //文件下载已完成
                .filter(cacheFile -> Files.exists(Paths.get(cacheFile.getFilePath()))) //临时文件是否存在
                .collect(Collectors.toSet());
        Set<CacheFile> notDoneFiles = cacheFiles
                .stream()
                .filter(cacheFile -> !doneFiles.contains(cacheFile))
                .collect(Collectors.toSet());
        if (notDoneFiles.isEmpty()) {
            cacheFiles.sort(CacheFile::compareTo);
            for (CacheFile cacheFile : cacheFiles) {
                Path cacheFilePath = Paths.get(cacheFile.getFilePath());
                System.out.printf("Range bytes=%d-%d%n", cacheFile.getStart(), cacheFile.getEnd());
                try {
                    InputStream inputStream = Files.newInputStream(cacheFilePath, StandardOpenOption.READ);
                    int tag;
                    while ((tag = inputStream.read(bufferByte)) > 0) {
                        outputStream.write(bufferByte, 0, tag);
                        outputStream.flush();
                    }
                    inputStream.close();
                } catch (IOException e) {
                    throw new HttpDownloadException(String.format("下载文件=%s合并临时文件失败", savePath.getFileName()), e);
                }
            }
            try {
                if (outputStream != null)
                    outputStream.close();
            } catch (IOException e) {
                throw new HttpDownloadException(String.format("下载文件=%s合并临时文件后关闭流失败", savePath.getFileName()), e);
            }
        } else {
            throw new HttpDownloadException(String.format("文件%s下载未完成, 未下载完成文件=%s", savePath.getFileName(), JSON.toJSONString(notDoneFiles.stream().map(CacheFile::getFilePath).collect(Collectors.toSet()))));
        }
    }

    /**
     * 不管是否异常 最后都要做的操作
     *
     * @param config 配置信息
     */
    @Override
    public void whenFinish(HttpDownloadConfig config) {
        DownloadConfigManager.writeDownloadConfig(config.getConfigFilePath(), config);
        OutputStream outputStream = config.getOutputStream();
        if (outputStream != null) {
            try {
                outputStream.flush();
            } catch (IOException e) {
                throw new HttpDownloadException(String.format("刷入文件=%s信息到输出文件流失败", Paths.get(config.getSavePath()).getFileName()), e);
            }
            try {
                outputStream.close();
            } catch (IOException e) {
                throw new HttpDownloadException(String.format("关闭文件=%s 输出文件流失败", Paths.get(config.getSavePath()).getFileName()), e);
            }
        }
    }

    /**
     * 当发生异常时的操作
     *
     * @param config 配置信息
     * @param cause  错误信息
     */
    @Override
    public void whenException(HttpDownloadConfig config, Throwable cause) {
        Path savePath = Paths.get(config.getSavePath());
        String url = config.getUrl();
        log.error(String.format("文件=%s url=%s 下载失败", savePath.getFileName(), url), cause);
        Path errorLogPath = Paths.get(config.getConfigFilePath() + ".error.log");
        try {
            OutputStream errorLogStream = Files.newOutputStream(errorLogPath, (Files.exists(errorLogPath) ? StandardOpenOption.APPEND : StandardOpenOption.CREATE));
            cause.printStackTrace(new PrintStream(errorLogStream, true, StandardCharsets.UTF_8.name()));
        } catch (IOException e) {
            log.error(String.format("写入错误日志信息失败=%s", errorLogPath.toAbsolutePath()), e);
        }
    }
}
